package Renderer;

import Errors.ErrorLog;
import HTMLParser.*;
import Renderer.HTMLDrawer.*;
import FrontEnd.GUI;
import Tree.*;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.io.*;
import java.lang.reflect.Constructor;

/**
 * This is the core class that sends instructions off to the other classes, and
 * actually controls the rendering of the HTML page on the screen.
 * @author Nathan Scully, James Johnstone, Tudor Pascu
 * @version FINAL
 */
public class HTMLRenderer {

    private Properties properties;
    private HTMLTreeGenerator treeGen;
    private ErrorLog errorLog;
    private int zoom;
    private ArrayList<String> pageHistory;
    private int pageIndex;
    private Stack propertyTextStack;
    private static String defaultPage = "index.html";
    private Tree tree;
    private JPanel page;
    private String currentPage;
    private int curX = 0;
    private int curY = 0;
    private int maxX = 0;
    private int indent = 0;
    private GUI updateGUI;
    private int heightOfLine = 0;
    private int curH1 = 0;
    private int curH2 = 0;
    private int curH3 = 0;

    /**
     * Constructor for loading the default page.
     * @param page The JScrollPane we are rendering to.
     */
    public HTMLRenderer(JPanel page, ErrorLog errorLog, GUI gui) {
        updateGUI = gui;
        this.errorLog = errorLog;
        properties();
        this.page = page;
        pageIndex = 0;
        zoom = 1;
        pageHistory = new ArrayList<String>();
        pageHistory.add(defaultPage);
        newPage(defaultPage);
    }

    /**
     * Constructor for loading a page other than the default one (i.e. if the
     * program is run from the command line with a page as the parameter).
     * @param page The JScrollPane we are rendering to.
     * @param file The address of the page to render.
     */
    public HTMLRenderer(JPanel page, String file, ErrorLog errorLog, GUI gui) {
        updateGUI = gui;
        this.errorLog = errorLog;
        properties();
        this.page = page;
        pageIndex = 0;
        zoom = 1;
        pageHistory = new ArrayList<String>();
        pageHistory.add(file);
        newPage(file);
    }

    /**
     * This loads the properties file to be used when traversing the tree.
     */
    public void properties() {
        properties = new Properties();
        try {
            properties.load(new FileInputStream(System.getProperty("user.dir") + System.getProperty("file.separator") + "RendererPropertiesFile.properties"));
        } catch (IOException ex) {
            errorLog.logError("Couldn't find RendererProperties.properties file.");
        }
    }

    /**
     * This is called every time a page needs to be displayed or redisplayed.
     * @param file  A string representing the path to the page being displayed.
     */
    private void newPage(String file) {
        errorLog.clearErrors();
        curX = 0;
        curY = 0;
        maxX = 0;
        heightOfLine = 0;
        treeGen = new HTMLTreeGenerator(file, errorLog);
        tree = treeGen.generateTree();
        currentPage = file;
        processTree(tree.getRoot());
        curH1 = 0;
        curH2 = 0;
        curH3 = 0;
        if (curX > maxX) {
            maxX = curX;
        }
    }

    /**
     * This will load the default page.
     */
    public void loadMainPage() {
        page = updateGUI.getFreshPage();
        trimHistory();
        pageHistory.add(defaultPage);
        pageIndex++;
        newPage(defaultPage);
    }

    /**
     * This will load a specified page, either specified by the link bar on the
     * GUI or a link rendered in a previously rendered page.
     * @param linkedPage The page to be displayed.
     */
    public void loadLinkedPage(String linkedPage) {
        page = updateGUI.getFreshPage();
        trimHistory();
        pageHistory.add(linkedPage);
        pageIndex++;
        newPage(linkedPage);
    }

    /**
     * This will ensure that, if you are currently not at the end of the history,
     * that anything after the page you are on in the history will be removed from
     * the history, and the current page becomes the new end of the history.
     */
    private void trimHistory() {
        if (pageIndex < pageHistory.size() - 1) {
            pageHistory = new ArrayList(pageHistory.subList(0, pageIndex + 1));
        }
    }

    /**
     * Returns if there is a next page in the history.
     * @return True or false.
     */
    public boolean hasNextPage() {
        return pageIndex < pageHistory.size() - 1;
    }

    /**
     * If there is a next page in the history, the next page will be rendered
     * and displayed.
     */
    public void getNextPage() {
        if (hasNextPage()) {
            page = updateGUI.getFreshPage();
            pageIndex++;
            newPage(pageHistory.get(pageIndex));
        }
    }

    /**
     * Returns if there is a previous page in the history.
     * @return True or false.
     */
    public boolean hasPreviousPage() {
        return pageIndex > 0;
    }

    /**
     * If there is a previous page in the history, the previous page will be
     * rendered and displayed.
     */
    public void getPreviousPage() {
        if (hasPreviousPage()) {
            page = updateGUI.getFreshPage();
            pageIndex--;
            newPage(pageHistory.get(pageIndex));
        }
    }

    /**
     * Increases scale factor on the zoom by 1, resets the renderer, and redraws
     * the current page.
     */
    public void zoomIn() {
        zoom++;
        indent = indent * zoom;
        resetRenderer();
        newPage(currentPage);
    }

    /**
     * Decreases scale factor on the zoom by 1, resets the renderer, and redraws
     * the current page, so long as it is possible to zoom out any further.
     */
    public void zoomOut() {
        if (zoom > 1) // This ensures that a zoom level of 0 or -ve numbers never happens.
        {
            zoom--;
            indent = indent * zoom;
            resetRenderer();
            newPage(currentPage);
        }
    }

    /**
     * Returns the current zoom level.
     * @return zoom An integer representing the current level of zoom.
     */
    public int getZoom() {
        return zoom;
    }

    /**
     * This will recursively process the entire tree that it is given, assuming
     * that the Node it is passed is the root of a tree or at least a subtree.
     * @param node The root of a tree or subtree to be processed.
     */
    public void processTree(Node node) {
        if (propertyTextStack == null) {
            propertyTextStack = new Stack(new TextProperty());
        }
        Drawer drawer = drawer = processNode(node);
        //Perform a depth first search, call appropriate drawers.
        while (node.hasChild() && !node.getChild().isExplored()) {
            processTree(node.getChild());
        }
        if (drawer != null) {
            drawer.postProcess();
        }
        node.setExplored(true);
    }

    /**
     * This will process each node individually. This method is usually called
     * by processTree; otherwise it is called during table processing.
     * @param node The Node to be processed.
     * @return A Drawer object to be used by processTree or table processing methods..
     */
    private Drawer processNode(Node node) {
        Drawer drawer = null;
        Class[] paramTypes = {Node.class, HTMLRenderer.class};
        String nodeType = node.getType().toLowerCase();
        String propString = properties.getProperty(nodeType);
        if (propString == null) {
            errorLog.logError("Node not found in properties file.");
        }
        try //Using reflection, the appropriate Drawer subclass is found.
        {
            Class nodeClass = Class.forName(propString);
            Constructor cons = nodeClass.getConstructor(paramTypes);
            Object[] params = {node, this};
            drawer = (Drawer) cons.newInstance(params);
            drawer.draw();
        } catch (ClassNotFoundException ex) {
            errorLog.logError("Unknown node found: " + propString);
        } catch (NoSuchMethodException ex) { // Most of these Exceptions will never happen.
        } catch (SecurityException ex) {
        } catch (InstantiationException ex) {
        } catch (IllegalAccessException ex) {
        } catch (IllegalArgumentException ex) {
        } catch (InvocationTargetException ex) {
        } catch (NullPointerException ex) {
        }
        return drawer;
    }

    /**
     * This method processes the text to be drawn on the page. This is deal with
     * text layout issues such as trimming white space from text (unless dealing
     * with preformatted text), word wrapping and arranging headings.
     * @param text
     */
    public void drawText(String text) {
        // Unless this text is preformatted, remove all unnecessary white space.
        if (!propertyTextStack.peek().isPre()) {
            String[] words = text.split(" ");
            text = "";
            for (int i = 0; i < words.length; i++) {
                if (!words[i].equals("")) {
                    text = text + " " + words[i];
                }
            }
            text = text.trim();
        }

        //Dealing with Titles
        if (propertyTextStack.peek().isTitle()) {
            updateGUI.setTitle(text);
            return;
        }

        //Deaing with Headings.
        if (propertyTextStack.peek().isH1()) {
            curH1++;
            if (curX != 0) {
                newLine();
            }
            text = curH1 + ". " + text;
        } else if (propertyTextStack.peek().isH2()) {
            curH2++;
            if (curX != 0) {
                newLine();
            }
            text = curH1 + "." + curH2 + ". " + text;
        } else if (propertyTextStack.peek().isH3()) {
            curH3++;
            if (curX != 0) {
                newLine();
            }
            text = curH1 + "." + curH2 + "." + curH3 + ". " + text;
        }

        //Sets the font.
        java.awt.Font currentFont = new java.awt.Font(propertyTextStack.peek().getFont(), propertyTextStack.peek().getStyle(), propertyTextStack.peek().getSize() * zoom);
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        FontMetrics metric = toolkit.getFontMetrics(currentFont);

        //Text wrapping algorithm.
        int start = 0;
        int finish = 0;
        int lastSpace = -1;

        while (finish < text.length()) {
            if (heightOfLine < (int) Math.round(currentFont.getSize() * 1.2)) {
                heightOfLine = (int) Math.round(currentFont.getSize() * 1.2); //hieghtOfLine maintains the max height of each line.
            }
            if (text.charAt(finish) == ' ') {
                lastSpace = finish;
            }
            finish++;
            if (metric.stringWidth(text.substring(start, finish)) >= updateGUI.getPageWidth() - (curX + indent) && lastSpace != start - 1) {
                JLabel textLabel = createLabel(text.substring(start, lastSpace), metric.stringWidth(text.substring(start, lastSpace)));
                textLabel.setFont(currentFont);
                start = lastSpace + 1;
                finish = lastSpace + 1;
                newLine();
            }
        }
        JLabel textLabel = createLabel(text.substring(start, finish), metric.stringWidth(text.substring(start, finish)) + 5);
        textLabel.setFont(currentFont);
        curX += textLabel.getWidth();
        page.validate();
    }

    /**
     * Creates a JLabel containing some text to be displayed.
     * @param subtext The text to be diplayed on a JLabel.
     * @param width How wide the JLabel should be.
     * @return
     */
    private JLabel createLabel(String subtext, int width) {
        JLabel textLabel = new JLabel(subtext);
        textLabel.setBounds(curX + indent, curY, width, (int) Math.round(propertyTextStack.peek().getSize() * 1.2 * zoom));
        textLabel.setForeground(getCurrentColour());
        textLabel.setVerticalAlignment(SwingConstants.TOP);
        if (propertyTextStack.peek().isUnderlined()) {
            drawLine(curX, curY + textLabel.getHeight() - zoom, width, zoom);
        }
        page.add(textLabel);
        if (propertyTextStack.peek().getIsLink()) {
            final String link = propertyTextStack.peek().getLink();
            // A link will require the JLabel to have a listener.
            textLabel.addMouseListener(new MouseAdapter() {

                public void mousePressed(MouseEvent evt) {
                    loadLinkedPage(link);
                    updateGUI.update();
                }
            });
        }
        return textLabel;
    }

    /**
     * Converts the colour specified by the TextProperty file at the top of the
     * Stack to a java.lang.Color value.
     * @return The java.lang.Color value corresponding to the current colour.
     */
    private Color getCurrentColour() {
        String colour = propertyTextStack.peek().getColor().toLowerCase();
        if (colour.equals("black")) {
            return Color.BLACK;
        } else if (colour.equals("blue")) {
            return Color.BLUE;
        } else if (colour.equals("cyan")) {
            return Color.CYAN;
        } else if (colour.equals("dark_gray") || colour.equals("darkgray")) {
            return Color.DARK_GRAY;
        } else if (colour.equals("gray")) {
            return Color.GRAY;
        } else if (colour.equals("green")) {
            return Color.GREEN;
        } else if (colour.equals("light_gray") || colour.equals("lightgray")) {
            return Color.LIGHT_GRAY;
        } else if (colour.equals("magenta")) {
            return Color.MAGENTA;
        } else if (colour.equals("orange")) {
            return Color.ORANGE;
        } else if (colour.equals("pink")) {
            return Color.PINK;
        } else if (colour.equals("red")) {
            return Color.RED;
        } else if (colour.equals("white")) {
            return Color.WHITE;
        } else if (colour.equals("yellow")) {
            return Color.YELLOW;
        }
        errorLog.logError("Font error: colour " + colour + " not supported.");
        return Color.BLACK;
    }

    /**
     * Increases the indentation currently being used.
     */
    public void increaseIndent() {
        indent += 25 * zoom;
    }

    /**
     * Increases the indentation currently being used.
     */
    public void descreaseIndent() {
        indent -= 25 * zoom;
    }

    /**
     * This creates a new line on the page. All things drawn on the page from this
     * point forward will appear below everything drawn up until now.
     */
    public void newLine() {
        if (curX > maxX) {
            maxX = curX;
        }
        curX = 0;
        if (heightOfLine == 0) {
            heightOfLine += (int) Math.round(propertyTextStack.peek().getSize() * 1.2 * zoom);
        }
        curY += heightOfLine;
        heightOfLine = 0;
    }

    /**
     * Resets the renderer, clearing the stack and removing everything that
     * is being displayed.
     */
    private void resetRenderer() {
        page = updateGUI.getFreshPage();
        propertyTextStack = new Stack(new TextProperty());
    }

    /**
     * This method is used for processing tables. It calculates the dimensions
     * of this cell.
     * @param cell The Node of the cell to be processed.
     * @return A two item int array of the dimensions of this cell.
     */
    private int[] findCellDims(Node cell) {
        int totalWidth = 0;
        int totalHeight = 0;
        int currentHeight = 0;
        while (cell.hasChild() && !cell.getChild().isExplored()) {
            String token = cell.getChild().getType().toLowerCase();
            if (token.equals("table")) {
                Node node = cell.getChild();
                //The line below contains a single line if-statement in the form (IF ? THEN : ELSE). Used to calculate the borderSize value.
                int borderSize = (((HTMLParser.HTMLTag.Table) node).getBorderSize() <= 1 ? ((HTMLParser.HTMLTag.Table) node).getBorderSize() : ((HTMLParser.HTMLTag.Table) node).getBorderSize() / 2);
                ArrayList<Integer>[] dim = findTableDims(cell.getChild());
                ArrayList<Integer> widths = dim[0];
                ArrayList<Integer> heights = dim[1];
                int tableWidth = 0;
                int tableHeight = 0;
                for (int i = 0; i < widths.size(); i++) {
                    tableWidth += widths.get(i) + (borderSize * 2 * zoom);
                }
                for (int j = 0; j < heights.size(); j++) {
                    tableHeight += heights.get(j) + (borderSize * 2 * zoom);
                }
                totalWidth += tableWidth + borderSize * zoom;
                if (tableHeight + borderSize * zoom > currentHeight) {
                    currentHeight = tableHeight + borderSize * zoom;
                }
            } else if (token.equals("text")) {
                String text = ((HTMLParser.HTMLTag.Text) (cell.getChild())).getText();
                java.awt.Font currentFont = new java.awt.Font(propertyTextStack.peek().getFont(), propertyTextStack.peek().getStyle(), propertyTextStack.peek().getSize() * zoom);
                Toolkit toolkit = Toolkit.getDefaultToolkit();
                FontMetrics metric = toolkit.getFontMetrics(currentFont);
                totalWidth += metric.stringWidth(text);
                if ((int) Math.round(propertyTextStack.peek().getSize() * 1.2 * zoom) > currentHeight) {
                    currentHeight = (int) Math.round(propertyTextStack.peek().getSize() * 1.2 * zoom);
                }
            } else if (token.equals("image")) {
                Node imageNode = cell.getChild();
                ImageIcon image = new ImageIcon(((HTMLParser.HTMLTag.Image) imageNode).getPath());
                totalWidth += image.getIconWidth();
                if (image.getIconHeight() > currentHeight) {
                    currentHeight = image.getIconHeight();
                }
            } else if (token.equals("p")) {
                totalHeight += currentHeight + 15;
                currentHeight = 0;
            } else if (token.equals("br")) {
                totalHeight += currentHeight;
                currentHeight = 0;
            } else {
                Drawer drawer = processNode(cell.getChild());
                if (drawer != null) {
                    drawer.postProcess();
                }
            }
            cell.getChild().setExplored(true);
        }
        totalHeight += currentHeight;
        int[] returnArray = new int[2];
        returnArray[0] = totalWidth;
        returnArray[1] = totalHeight;
        return returnArray;
    }

    /**
     * This method is used for processing tables. It calculates the dimensions
     * of this table based on the results from the findCellDims method.
     * @param cell The Node of the table to be processed.
     * @return A two item array of ArrayLists of Integers of the dimensions of this table.
     */
    private ArrayList<Integer>[] findTableDims(Node root) {
        ArrayList<Integer> maxWidths = new ArrayList<Integer>();
        ArrayList<Integer> maxHeights = new ArrayList<Integer>();
        int curRow = -1;
        int curCol = -1;

        while (root.hasChild() && !root.getChild().isExplored()) {
            if (root.getChild().getType().toLowerCase().equals("tr")) //If this node is a row.
            {
                curRow++;
                curCol = -1;
                while (root.getChild().hasChild() && !root.getChild().getChild().isExplored()) {
                    if (root.getChild().getChild().getType().toLowerCase().equals("td")) //If this node is a cell.
                    {
                        //Find the maximum width and height of the widths and heights of the cells.
                        curCol++;
                        int[] dim = findCellDims(root.getChild().getChild());
                        if (curCol >= maxWidths.size()) {
                            maxWidths.add(dim[0]);
                        } else {
                            if (dim[0] > maxWidths.get(curCol)) {
                                maxWidths.remove(curCol);
                                maxWidths.add(curCol, dim[0]);
                            }
                        }
                        if (curRow >= maxHeights.size()) {
                            maxHeights.add(dim[1]);
                        } else {
                            if (dim[1] > maxHeights.get(curRow)) {
                                maxHeights.remove(curRow);
                                maxHeights.add(curRow, dim[1]);
                            }
                        }
                    }
                    root.getChild().getChild().setExplored(true);
                }
            }
            root.getChild().setExplored(true);
        }

        ArrayList<Integer>[] returnArray = new ArrayList[2];
        returnArray[0] = maxWidths;
        returnArray[1] = maxHeights;
        return returnArray;
    }

    /**
     * This method generates the table, recusively for nested tables. This method
     * will add everything to a JLabel, which can then either be drawn to the page,
     * or within another table's JLabel.
     * @param root The root of the subtree specifying the table currently under consideration.
     * @param widths The widths to be used when drawing this table to a JLabel.
     * @param heights The heights to be used when drawing this table to a JLabel.
     * @return The JLabel containing the drawn table.
     */
    public JLabel generateTable(Node root, ArrayList<Integer> widths, ArrayList<Integer> heights) {
        //First, the Nodes need to be reset so that this subtree can be re-traversed (it was traversed when calculating the dimensions of the table).
        Node.resetExplored(root);
        JLabel table = new JLabel();
        //The line below contains a single line if-statement in the form (IF ? THEN : ELSE). Used to calculate the borderSize value.
        int borderSize = (((HTMLParser.HTMLTag.Table) root).getBorderSize() <= 1 ? ((HTMLParser.HTMLTag.Table) root).getBorderSize() : ((HTMLParser.HTMLTag.Table) root).getBorderSize() / 2);
        int curRow = -1;
        int curCol = -1;
        int curCellX = borderSize * zoom;
        int curCellY = borderSize * zoom;
        int curTableX = 0;
        int curTableY = 0;
        int maxTableX = 0;
        while (root.hasChild() && !root.getChild().isExplored()) {
            String tokenRow = root.getChild().getType().toLowerCase();
            if (tokenRow.equals("tr")) //If the node is a row.
            {
                curCol = -1;
                curRow++;

                while (root.getChild().hasChild() && !root.getChild().getChild().isExplored()) {
                    String tokenCell = root.getChild().getChild().getType().toLowerCase();
                    if (tokenCell.equals("td")) // If the node is a cell.
                    {
                        curCol++;
                        JLabel thisCell = new JLabel(); //This cell it a JLabel, which will be drawn to a table JLabel.

                        while (root.getChild().getChild().hasChild() && !root.getChild().getChild().getChild().isExplored()) {
                            String token = root.getChild().getChild().getChild().getType().toLowerCase();
                            if (token.equals("text")) {
                                java.awt.Font currentFont = new java.awt.Font(propertyTextStack.peek().getFont(), propertyTextStack.peek().getStyle(), propertyTextStack.peek().getSize() * zoom);
                                Toolkit toolkit = Toolkit.getDefaultToolkit();
                                FontMetrics metric = toolkit.getFontMetrics(currentFont);
                                String text = ((HTMLParser.HTMLTag.Text) (root.getChild().getChild().getChild())).getText();
                                int totalWidth = metric.stringWidth(text);
                                JLabel thisText = new JLabel(text);
                                thisText.setBounds(curCellX, curCellY, totalWidth + zoom, (int) Math.round(propertyTextStack.peek().getSize() * 1.2 * zoom));
                                thisText.setFont(currentFont);
                                thisCell.add(thisText);
                                curCellX += thisText.getWidth();
                            } else if (token.equals("image")) {
                                Node node = root.getChild().getChild().getChild();
                                ImageIcon image = new ImageIcon(((HTMLParser.HTMLTag.Image) node).getPath());
                                JLabel thisImage = new JLabel(image);
                                thisImage.setBounds(curCellX, curCellY, image.getIconWidth(), image.getIconHeight());
                                thisCell.add(thisImage);
                                curCellX += thisImage.getWidth();
                            } else if (token.equals("table")) {
                                ArrayList<Integer>[] dim = findTableDims(root.getChild().getChild().getChild());
                                JLabel thisTable = generateTable(root.getChild().getChild().getChild(), dim[0], dim[1]);
                                thisTable.setBounds(curCellX, curCellY, widths.get(curCol) + zoom, heights.get(curRow) + zoom);
                                thisCell.add(thisTable);
                                curCellX += thisTable.getWidth();
                            } else if (token.equals("p")) {
                                curCellY += (int) Math.round(propertyTextStack.peek().getSize() * 1.2 * zoom) + 15;
                                curCellX = borderSize * zoom;
                            } else if (token.equals("br")) {
                                curCellY += (int) Math.round(propertyTextStack.peek().getSize() * 1.2 * zoom);
                                curCellX = borderSize * zoom;
                            } else {
                                Drawer drawer = processNode(root.getChild().getChild().getChild());
                                if (drawer != null) {
                                    drawer.postProcess();
                                }
                            }
                            root.getChild().getChild().getChild().setExplored(true);
                        }
                        thisCell.setBounds(curTableX + (borderSize * zoom), curTableY + (borderSize * zoom), widths.get(curCol) + (borderSize * 2 * zoom), heights.get(curRow) + (borderSize * 2 * zoom));
                        thisCell.setBorder(BorderFactory.createLineBorder(Color.BLACK, borderSize * zoom));
                        table.add(thisCell);
                        curTableX += widths.get(curCol) + (borderSize * 2 * zoom);
                    }
                    root.getChild().getChild().setExplored(true);
                    curCellX = borderSize * zoom;
                    curCellY = borderSize * zoom;
                }
                if (curTableX > maxTableX) {
                    maxTableX = curTableX;
                }
                curTableX = 0;
                curTableY += heights.get(curRow) + (borderSize * 2 * zoom);
            }
            root.getChild().setExplored(true);
        }
        //Calculating the size for the table JLabel.
        int tableWidth = 0;
        for (int i = 0; i < widths.size(); i++) {
            tableWidth += widths.get(i) + (borderSize * 2 * zoom);
        }
        int tableHeight = 0;
        for (int j = 0; j < heights.size(); j++) {
            tableHeight += heights.get(j) + (borderSize * 2 * zoom);
        }
        table.setBounds(curX, curY, tableWidth + (borderSize * 2 * zoom), tableHeight + (borderSize * 2 * zoom));
        table.setBorder(BorderFactory.createLineBorder(Color.BLACK, borderSize * zoom));
        heightOfLine = curTableY + (borderSize * 2 * zoom);
        if (maxTableX + (borderSize * 2 * zoom) > maxX) {
            maxX = maxTableX + (borderSize * 2 * zoom);
        }
        return table;
    }

    /**
     * This will draw the table by first getting the dimensions of the tree, then
     * drawing the tree to a JLabel, which is then finally added to the page.
     * @param root The root of the subtree containing the table (discluding nested tables).
     */
    public void drawTable(Node root) {
        heightOfLine = 0;
        ArrayList<Integer>[] dim = findTableDims(root);
        page.add(generateTable(root, dim[0], dim[1]));
        page.validate();
    }

    /**
     * This allows the GUI to know the height of the page.
     * @return The height of the page.
     */
    public int getMaxY() {
        return curY + heightOfLine;
    }

    /**
     * This allows the GUI to know the width of the page.
     * @return The width of the page.
     */
    public int getMaxX() {
        return maxX;
    }

    /**
     * This allows the GUI to know the page that is currently being displayed.
     * @return The current page as a String.
     */
    public String getCurrentPage() {
        return currentPage;
    }

    /**
     * This will place an image on the page.
     * @param image The IconImage containing the image to be displayed.
     */
    public void placeImage(ImageIcon image) {
        JLabel imageLabel = new JLabel(image);
        imageLabel.setBounds(curX + indent, curY, image.getIconWidth(), image.getIconHeight());
        if (heightOfLine < image.getIconHeight()) {
            heightOfLine = image.getIconHeight();
        }
        curX += image.getIconWidth();
        page.add(imageLabel);
        page.validate();
    }

    /**
     * This will draw a line across the page; used for HRule tags.
     */
    public void drawLine() {
        JLabel lineLabel = new JLabel();
        lineLabel.setBackground(Color.BLACK);
        lineLabel.setOpaque(true);
        lineLabel.setBounds(zoom, curY, updateGUI.getPageWidth() - 10, 2 * zoom);
        page.add(lineLabel);
        curY += 2 * zoom;
    }

    /**
     * This will draw a line in a specific place, usually used for underlining
     * text. The parameters specify it's size and location.
     * @param x The x co-ord of the line.
     * @param y The y co-ord of the line.
     * @param height The height of the line.
     * @param width The width of the line.
     */
    public void drawLine(int x, int y, int height, int width) {
        JLabel lineLabel = new JLabel();
        lineLabel.setBackground(getCurrentColour());
        lineLabel.setOpaque(true);
        lineLabel.setBounds(x + indent, y, height, width);
        page.add(lineLabel);
    }

    /**
     * Returns the error log.
     * @return The ErrorLog.
     */
    public ErrorLog getErrors() {
        return errorLog;
    }

    /**
     * Returns the property text stack.
     * @return The Stack containing TextProperty objects.
     */
    public Stack getStack() {
        return propertyTextStack;
    }

    /**
     * Returns the GUI.
     * @return The GUI.
     */
    public GUI getGUI() {
        return updateGUI;
    }
}